import types
import inspect
import traceback
import textwrap
from repr import repr as limited_repr


#
# doc nodes
#
class DocNode(object):
    _defaults = {}
    
    def __init__(self, *elems, **kw):
        self._elems = []
        for e in elems:
            self.append(e)
        self.__dict__.update(self._defaults)
        self.__dict__.update(kw)
    
    def __str__(self):
        return self.to_text()
    
    def append(self, obj):
        assert isinstance(obj, (basestring, DocNode)), \
            "%r must be a string or a DocNode" % (obj,)
        self._elems.append(obj)
    
    def to_html(self):
        return "<html><header></header><body>" + self._to_html() + "</body></html>"
    
    def to_text(self, linewrap = None):
        text = self._to_text()
        if linewrap is not None:
            text = "\n".join(textwrap.wrap(text, linewrap))
        return text
    
    def _concat_text(self):
        text = []
        for e in self._elems:
            if isinstance(e, basestring):
                t = e
            else:
                t = e._to_text()
            text.append(t)
            if t.rstrip() == t:
                text.append(" ")
        return "".join(text)
    
    def _to_text(self):
        raise NotImplementedError()
    
    def _to_html(self):
        raise NotImplementedError()

class Section(DocNode):
    _defaults = {"indent" : True}
    
    def _to_text(self):
        text = self._concat_text()
        if self.indent:
            indented = []
            for l in text.splitlines():
                indented.append("    ")
                indented.append(l)
                indented.append("\n")
            text = "".join(indented)
        return text

class Paragraph(DocNode):
    def _to_text(self):
        return self._concat_text() + "\n"

class Title(DocNode):
    def _to_text(self):
        return self._concat_text()

class Reference(DocNode):
    def __init__(self, obj, reprfunc = repr):
        DocNode.__init__(self, obj = obj, reprfunc = reprfunc)
    
    def _to_text(self):
        return self.reprfunc(self.obj)

class Code(DocNode):
    def _to_text(self):
        pass

#
# helpers
#
def get_func_decl(obj):
    try:
        args = inspect.getargspec(obj)
    except TypeError:
        argstext = "(...)"
    else:
        argstext = inspect.formatargspec(*args)
    return obj.__name__ + argstext

def parse_formatted_docstring(text):
    """perhaps in the future we'll want to add some simple markup langauge
    to docstrings.  if so, this function will be used to parse this markup
    and generate the corresponding DocNode objects"""
    return text

def get_doc(obj):
    doc = inspect.getdoc(obj)
    if doc is None:
        doc = inspect.getcomments(obj)
    if doc is None:
        doc = "..."
    return parse_formatted_docstring(doc)

def pager(text):
    """prints the text through the system pager (platform dependent).
    if it fails to find a pager, uses stdout"""
    print text


#
# doc generators
# blah blah
#
def module_doc(obj):
    doc = Section(
        Paragraph(
            "Help on module",
            Title(obj.__name__),
        ),
        Paragraph(),
        Paragraph("FILE"),
        Section(getattr(obj, "__file__", "(built-in)")), 
        Paragraph(),
        Paragraph("DESCRIPTION"),
        Section(get_doc(obj)), 
        indent = False
    )
    
    classes = []
    functions = []
    data = []
    for name in dir(obj):
        subobj = getattr(obj, name)
        if inspect.isroutine(subobj):
            functions.append(function_doc(subobj))
        elif inspect.isclass(subobj):
            classes.append(class_doc(subobj))
        else:
            data.append(Paragraph(name, "=", limited_repr(subobj)))
    
    if classes:
        doc.append(Paragraph())
        doc.append(Paragraph("CLASSES"))
        doc.append(Section(*classes))
    if functions:
        doc.append(Paragraph())
        doc.append(Paragraph("FUNCTIONS"))
        doc.append(Section(*functions))
    if data:
        doc.append(Paragraph())
        doc.append(Paragraph("DATA"))
        doc.append(Section(*data))
    
    return doc

def package_doc(obj):
    pass

def class_doc(obj):
    pass

def builtin_function_doc(obj):
    return Section(
        Paragraph(
            "Help on builtin function",
            Title(obj.__name__),
            "in module",
            Reference(inspect.getmodule(obj), lambda o: o.__name__)
        ),
        Paragraph(), 
        Paragraph(get_func_decl(obj)), 
        Section(get_doc(obj)),
        indent = False,
    )

def function_doc(obj):
    return Section(
        Paragraph(
            "Help on function",
            Title(obj.__name__),
            "in module",
            Reference(inspect.getmodule(obj), lambda o: o.__name__)
        ),
        Paragraph(), 
        Paragraph(get_func_decl(obj)), 
        Section(get_doc(obj)),
        indent = False,
    )

def method_doc(obj):
    return Section(
        Paragraph(
            "Help on method",
            Title(obj.__name__),
            "in module",
            Reference(inspect.getmodule(obj), lambda o: o.__name__)
        ),
        Paragraph(), 
        Paragraph(get_func_decl(obj)), 
        Section(get_doc(obj)),
        indent = False,
    )

def traceback_doc(obj):
    pass

def classic_instance_doc(obj):
    pass

def generic_doc(obj):
    return repr(obj)

def generate_object_doc(obj):
    if hasattr(obj, "__help__"):
        return obj.__help__()
    if inspect.isclass(obj):
        return class_doc(obj)
    if inspect.ismodule(obj):
        return module_doc(obj)
    if inspect.ismethod(obj):
        if hasattr(obj, "__path__"):
            return package_doc(obj)
        else:
            return method_doc(obj)
    if inspect.isbuiltin(obj):
        return builtin_function_doc(obj)
    if inspect.isfunction(obj):
        return function_doc(obj)
    if inspect.istraceback(obj):
        return traceback_doc(obj)
    if isinstance(obj, types.InstanceType):
        return classic_instance_doc(obj)
    return generic_doc(obj)

def topic_doc(topic):
    # topic may be:
    # * a keyword
    # * a module name
    # * "topics"
    # * one of the names in the topics list
    return Paragraph("topic not found")

#
# API
#
def interactive_help():
    pass

def help(obj = None):
    if obj is None:
        interactive_help()
    elif isinstance(obj, basestring):
        pager(topic_doc(obj).to_text())
    else:
        pager(generate_object_doc(obj).to_text())


#
# test
#
if __name__ == "__main__":
    import sys
    help(sys)



















